package session

import (
	"mi/compiler/ast"
	"mi/compiler/scanner"
	"mi/compiler/token"
	"path/filepath"
	"reflect"
)

func DetectLanguage(langID, filename string) FileKind {
	switch langID {
	case "mi":
		return Mi
	case "mi.mod":
		return Mod
	case "go.sum":
		return Sum
	}
	// Fallback to detecting the language based on the file extension.
	switch filepath.Ext(filename) {
	case ".mod":
		return Mod
	case ".sum":
		return Sum
	default: // fallback to Go
		return Mi
	}
}

func (k FileKind) String() string {
	switch k {
	case Mod:
		return "go.mod"
	case Sum:
		return "go.sum"
	default:
		return "go"
	}
}

// Node operator
func getTokenPosOfNode(node ast.Node, sourceFile *ast.SourceFile, includeDocComment bool) int {
	// With nodes that have no width (i.e. 'Missing' nodes), we actually *don't*
	// want to skip trivia because this will launch us forward to the next token.
	if ast.NodeIsMissing(node) {
		return node.Pos()
	}

	if ast.IsDocNode(node) {
		if sourceFile == nil {
			sourceFile = ast.GetSourceFileOfNode(node)
		}
		return scanner.SkipTrivia(sourceFile.Text, node.Pos(), false, true)
	}

	if includeDocComment && len(node.GetDocuments()) > 0 {
		return getTokenPosOfNode(node.GetDocuments()[0], sourceFile, includeDocComment)
	}

	// For a syntax list, it is possible that one of its children has JSDocComment nodes, while
	// the syntax list itself considers them as normal trivia. Therefore if we simply skip
	// trivia for the list, we may have skipped the JSDocComment as well. So we should process its
	// first child to determine the actual offset of its first token.
	if ast.IsSyntaxList(node) {
		if children := getNodeChildren(node, sourceFile); len(children) > 0 {
			return getTokenPosOfNode(children[0], sourceFile, includeDocComment)
		}
	}

	if sourceFile == nil {
		sourceFile = ast.GetSourceFileOfNode(node)
	}
	return scanner.SkipTrivia(sourceFile.Text, node.Pos(), false, false)
}

func getNodeStart(node ast.Node, sourceFile *ast.SourceFile, includeJsDocComment bool) int {
	return getTokenPosOfNode(node, sourceFile, includeJsDocComment)
}

func getNodeFullStart(node ast.Node) int {
	return node.Pos()
}

func getNodeEnd(node ast.Node) int {
	return node.End()
}

func getNodeWidth(node ast.Node, sourceFile *ast.SourceFile) int{
	return node.End() - getNodeStart(node, sourceFile, false)
}

func getNodeText(node ast.Node, sourceFile *ast.SourceFile) string {
	if sourceFile == nil {
		sourceFile = ast.GetSourceFileOfNode(node)
	}
	return string(sourceFile.Text[getNodeStart(node, sourceFile, false):getNodeEnd(node)])
}

const _children = "_children"
func getNodeChildrenCache(node ast.Node) []ast.Node{
	val, _ := node.Get(_children).([]ast.Node)
	return val
}

func createTokenNode(tok token.Token, pos, end int, parent ast.Node) ast.Node {
	var node = new(ast.TokenNode)
	node.Token = tok
	node.SetPos(pos)
	node.SetEnd(end)
	node.SetParent(parent)
	return node
}

func createSyntaxList(pos, end int, parent ast.Node) ast.Node {
	var node = new(ast.SyntaxList)
	node.SetPos(pos)
	node.SetEnd(end)
	node.SetParent(parent)
	return node
}

func nodeIndexOf(nodes []ast.Node, node ast.Node) int {
	for i, n := range nodes {
		if node == n {
			return i
		}
	}
	return -1
}

var emptyNodes = make([]ast.Node, 0)
var scan = scanner.NewScanner(func(opt *scanner.Optional) {
	opt.SkipTrivia = true
})
func getNodeChildren(curNode ast.Node, sourceFile *ast.SourceFile) []ast.Node {
	var children = getNodeChildrenCache(curNode)
	if children == nil {
		var addSyntheticNodes = func(pos, end int) ([]ast.Node, int) {
			var nodes []ast.Node
			scan.SetTextPos(pos)
			for pos < end {
				var tok = scan.Scan()
				var textPos = scan.GetTextPos()
				if textPos <= end {
					nodes = append(nodes, createTokenNode(tok, pos, textPos, curNode))
				}
				pos = textPos
			}
			return nodes, pos
		}

		var createSyntaxList = func(nodes ast.List) ast.Node {
			var list = createSyntaxList(nodes.Pos(), nodes.End(), curNode)
			var children, temps []ast.Node
			var pos = nodes.Pos()
			for i := 0; i < nodes.Len(); i ++ {
				var node = nodes.NodeAt(i)
				if pos < node.Pos() {
					temps, pos = addSyntheticNodes(pos, node.Pos())
					children = append(children, temps...)
				}
				children = append(children, node)
				pos = node.End()
			}

			if pos < nodes.End() {
				temps, pos = addSyntheticNodes(pos, nodes.End())
				children = append(children, temps...)
			}
			list.Set(_children, children)
			return list
		}

		if sourceFile == nil {
			sourceFile = ast.GetSourceFileOfNode(curNode)
		}
		scan.SetText(sourceFile.Text, -1, -1)
		var pos = curNode.Pos()
		var temps []ast.Node
		var processNode = func (child ast.Node) bool {
			if pos < child.Pos() {
				temps, pos = addSyntheticNodes(pos, child.Pos())
				children = append(children, temps...)
			}
			children = append(children, child)
			pos = child.End()

			return false
		}
		var processNodes = func (nodes ast.List) bool {
			if pos < nodes.Pos() {
				temps, pos = addSyntheticNodes(pos, nodes.Pos())
				children = append(children, temps...)
			}
			children = append(children, createSyntaxList(nodes))
			pos = nodes.End()

			return false
		}
		// DocComments need to be the first children
		for _, document := range curNode.GetDocuments() {
			processNode(document)
		}
		
		// For syntactic classifications, all trivia are classcified together, including jsdoc comments.
		// For that to work, the jsdoc comments should still be the leading trivia of the first child.
		// Restoring the scanner offset ensures that.
		pos = curNode.Pos()
		ast.ForEachChild(curNode, processNode, processNodes)
		if pos < curNode.End() {
			temps, _ = addSyntheticNodes(pos, curNode.End())
			children = append(children, temps...)
		}
		scan.SetText(nil, -1, -1)

		if children == nil {
			children = emptyNodes
		}
		curNode.Set(_children, children)
	}

	return children
}

func isJumpStatementTarget(node ast.Node) bool {
	return ast.IsIdentifier(node) &&
		ast.IsBranchStatement(node.Parent()) &&
		node.(*ast.BranchStatement).Label == node
}

func isLabelOfLabeledStatement(node ast.Node) bool {
	return ast.IsIdentifier(node) &&
		ast.IsLabeledStatement(node.Parent()) &&
		node.(*ast.LabeledStatement).Label == node
}

func isLabelName(node ast.Node) bool {
	return isLabelOfLabeledStatement(node) || isJumpStatementTarget(node)
}

func isToken(node ast.Node) bool {
	switch node.(type) {
	case *ast.LiteralExpression,
		*ast.Identifier,
		*ast.TokenNode:
		return true
	}
	return false
}

func nodeHasTokens(node ast.Node) bool {
	// If we have a token or ast that has a non-zero width, it must have tokens.
	// Note, that getWidth() does not take trivia into account.
	return getNodeWidth(node, nil) != 0
}

func positionBelongsToNode(candidate ast.Node, position int, sourceFile *ast.SourceFile) bool {
	return candidate.End() > position || !isCompletedNode(candidate, sourceFile)
}

func nodeEndsWith(node ast.Node, expectedLastToken token.Token, sourceFile *ast.SourceFile) bool {
	var children =getNodeChildren(node, sourceFile)
	if len(children) > 0 {
		var last = children[len(children)-1]
		// If it does not match and it is a semicolon, then the previous one is judged
		return ast.IsTokenNodeAndEqualKind(last, expectedLastToken) ||
			len(children) > 1 && ast.IsTokenNodeAndEqualKind(last, token.Semicolon) &&
			ast.IsTokenNodeAndEqualKind(children[len(children)-2], expectedLastToken)
	}
	return false
}

func findChildOfKind(n ast.Node, cb func (node ast.Node) bool, sourceFile *ast.SourceFile) ast.Node {
	for _, child := range getNodeChildren(n, sourceFile) {
		if cb(child) {
			return child
		}
	}
	return nil
}

type ListItemInfo struct {
	ListItemIndex int
	List ast.Node
}

func findListItemInfo(node ast.Node) *ListItemInfo {
	var list = findContainingList(node)

	// It is possible at this point for syntaxList to be undefined, either if
	// ast.parent had no list child, or if none of its list children contained
	// the span of ast. If this happens, return undefined. The caller should
	// handle this case.
	if list == nil {
		return nil
	}

	var children = getNodeChildren(list, nil)
	var listItemIndex = ast.NodeIndexOf(children, node)
	return &ListItemInfo{
		ListItemIndex: listItemIndex,
		List:          list,
	}
}

func findContainingList(node ast.Node) ast.Node {
	// The ast might be a list element (nonsynthetic) or a comma (synthetic). Either way, it will
	// be parented by the container of the SyntaxList, not the SyntaxList itself.
	// In order to find the list item index, we first need to locate SyntaxList itself and then search
	// for the position of the relevant ast (or comma).

	var parentChildren = getNodeChildren(node.Parent(), nil)
	var syntaxList ast.Node
	for _, child := range parentChildren {
		if ast.IsSyntaxList(child) && child.Pos() <= node.Pos() && child.End() >= node.End() {
			syntaxList = child
			break
		}
	}

	// Either we didn't find an appropriate list, or the list must contain us.
	assert(syntaxList == nil || ast.NodeIndexOf(getNodeChildren(syntaxList, nil), node) != -1)
	return syntaxList
}

func hasChildOfKind(n ast.Node, cb func(node ast.Node) bool, sourceFile *ast.SourceFile) bool {
	return findChildOfKind(n, cb, sourceFile) != nil
}

func isCompletedNode(node ast.Node, sourceFile *ast.SourceFile) bool {
	if ast.NodeIsMissing(node) {
		return false
	}

	switch n := node.(type) {
	case *ast.ClassDeclaration,
		*ast.InterfaceDeclaration,
		*ast.EnumDeclaration,
		*ast.BlockStatement:
		return nodeEndsWith(n, token.CloseBrace, sourceFile)

	case *ast.CatchClause:
		return isCompletedNode(n.Block, sourceFile)
	case *ast.NewObjectExpression:
		return nodeEndsWith(n, token.CloseParen, sourceFile)
	case *ast.CallExpression:
		return nodeEndsWith(n, token.CloseParen, sourceFile)
	case *ast.ConstructorDeclaration,
		*ast.MethodDeclaration,
		*ast.ArrowFunctionDeclaration:
		if node.(ast.FunctionLike).GetBody() != nil {
			return isCompletedNode(node.(ast.FunctionLike).GetBody(), sourceFile)
		}
		if node.(ast.FunctionLike).GetType() != nil {
			return isCompletedNode(node.(ast.FunctionLike).GetType(), sourceFile)
		}

		// Even though type parameters can be unclosed, we can get away with
		// having at least a closing paren.
		return hasChildOfKind(n, func(node ast.Node) bool {
			return ast.IsTokenNodeAndEqualKind(node, token.CloseParen)
		}, sourceFile)

	case *ast.IfStatement:
		if n.ElseStatement != nil {
			return isCompletedNode(n.ElseStatement, sourceFile)
		}
		return isCompletedNode(n.ThenStatement, sourceFile)
	case *ast.ExpressionStatement:
		return isCompletedNode(n.Expression, sourceFile) ||
			hasChildOfKind(n, func(node ast.Node) bool {
				return ast.IsTokenNodeAndEqualKind(node, token.CloseParen)
			}, nil)
	case *ast.CaseClause:
		// there is no such thing as terminator token for CaseClause/DefaultClause so for simplicity always consider them non-completed
		return false
	case *ast.ForStatement:
		return isCompletedNode(n.Body, sourceFile)
	case *ast.ForOfStatement:
		return isCompletedNode(n.Body, sourceFile)
	case *ast.WhileStatement:
		return isCompletedNode(n.Body, sourceFile)
	case *ast.DoStatement:
		// rough approximation: if DoStatement has While keyword - then if ast is completed is checking the presence of ')';
		var hasWhileKeyword = findChildOfKind(n, func(node ast.Node) bool {
			return ast.IsTokenNodeAndEqualKind(node, token.WhileKeyword)
		}, sourceFile)
		if hasWhileKeyword != nil {
			return nodeEndsWith(n, token.CloseParen, sourceFile)
		}
		return isCompletedNode(n.Body, sourceFile)
	case *ast.BinaryExpression:
		return isCompletedNode(n.Right, sourceFile)
	case *ast.ConditionalExpression:
		return isCompletedNode(n.WhenFalse, sourceFile)
	default:
		return true
	}
}

func findPrecedingToken(position int, sourceFile *ast.SourceFile, startNode ast.Node) ast.Node {
	findRightmostChildNodeWithTokens := func (children []ast.Node, exclusiveStartPosition int) ast.Node {
		for i := exclusiveStartPosition - 1; i >= 0; i-- {
			if nodeHasTokens(children[i]) {
				return children[i]
			}
		}
		return nil
	}

	var findRightmostToken func (n ast.Node) ast.Node
	findRightmostToken = func (n ast.Node) ast.Node {
		if isToken(n) {
			return n
		}

		var children = getNodeChildren(n, nil)
		var candidate = findRightmostChildNodeWithTokens(children, len(children))
		if candidate != nil {
			return findRightmostToken(candidate)
		}
		return nil
	}

	var find func (n ast.Node) ast.Node
	find = func (n ast.Node) ast.Node {
		if isToken(n) {
			return n
		}

		var children = getNodeChildren(n, nil)
		for i := 0; i < len(children); i++ {
			var child = children[i]
			// condition 'offset < child.end' checks if child ast end after the offset
			// in the example below this condition will be false for 'aaaa' and 'bbbb' and true for 'ccc'
			// aaaa___bbbb___$__ccc
			// after we found child ast with end after the offset we check if start of the ast is after the offset.
			// if yes - then offset is in the trivia and we need to look into the previous child to find the token in question.
			// if no - offset is in the ast itself so we should recurse in it.
			// NOTE: JsxText is a weird kind of ast that can contain only whitespaces (since they are not counted as trivia).
			// if this is the case - then we should assume that token in question is located in previous child.
			if position < child.End() && nodeHasTokens(child) {
				var start = getNodeStart(child, sourceFile, false)
				var lookInPreviousChild = start >= position // cursor in the leading trivia

				if lookInPreviousChild {
					// actual start of the ast is past the offset - previous token should be at the end of previous child
					var candidate = findRightmostChildNodeWithTokens(children, i)
					if candidate != nil {
						return findRightmostToken(candidate)
					}
					return nil
				} else {
					// candidate should be in this ast
					return find(child)
				}
			}
		}


		if startNode == nil && !ast.IsSourceFile(n) {
			panic("startNode is nil and kind as session file")
		}

		// Here we know that none of child token nodes embrace the offset,
		// the only known case is when offset is at the end of the file.
		// Try to find the rightmost token in the file without filtering.
		// Namely we are skipping the check: 'offset < ast.end'
		if len(children) > 0 {
			var candidate = findRightmostChildNodeWithTokens(children, len(children))
			if candidate != nil {
				return findRightmostToken(candidate)
			}
		}
		return nil
	}

	if startNode != nil {
		return find(startNode)
	} else {
		return find(sourceFile)
	}
}

func isKeyword(node ast.Node) bool {
	switch n := node.(type) {
	case *ast.TokenNode:
		return n.Token.IsKeyword()
	}
	return false
}

func isWord(node ast.Node) bool {
	return ast.IsIdentifier(node) || isKeyword(node)
}

func isName(node ast.Node) bool {
	return isWord(node)
}

// Gets the token whose text has range [start, end) and
// offset >= start and (offset < end or (offset === end && token is keyword or identifier))
func getTouchingWord(sourceFile *ast.SourceFile, position int, includeJsDocComment bool) ast.Node {
	return getTouchingToken(sourceFile, position, func(n ast.Node) bool {
		return isWord(n)
	}, includeJsDocComment)
}

// Gets the token whose text has range [start, end) and offset >= start
// and (offset < end or (offset === end && token is keyword or identifier or numeric/string literal))
func getTouchingName(sourceFile *ast.SourceFile, position int, includeJsDocComment bool) ast.Node {
	return getTouchingToken(sourceFile, position, func(n ast.Node) bool {
		return isName(n)
	}, includeJsDocComment)
}

// Returns the token if offset is in [start, end) or if offset === end and includeItemAtEndPosition(token) === true
func getTouchingToken(sourceFile *ast.SourceFile, position int, includeItemAtEndPosition func (n ast.Node) bool, includeJsDocComment bool) ast.Node {
	return getTokenAtPositionWorker(sourceFile, position,false, includeItemAtEndPosition, includeJsDocComment)
}

// Returns a token if offset is in [start-of-leading-trivia, end)
func getTokenAtPosition(sourceFile *ast.SourceFile, position int, includeJsDocComment bool) ast.Node {
	return getTokenAtPositionWorker(sourceFile, position,true, nil, includeJsDocComment)
}

// Get the token whose text contains the offset
func getTokenAtPositionWorker(sourceFile *ast.SourceFile, position int, allowPositionInLeadingTrivia bool, includeItemAtEndPosition func(n ast.Node) bool, includeDocComment bool) ast.Node {
	var current ast.Node = sourceFile
outer:
	for true {
		if isToken(current) {
			// exit early
			return current
		}

		if includeDocComment {
			for _, child := range getNodeChildren(current, nil) {
				if !ast.IsDocNode(child) {
					continue
				}

				var start int
				if allowPositionInLeadingTrivia {
					start = getNodeFullStart(child)
				} else {
					start = getNodeStart(child, sourceFile, includeDocComment)
				}

				if start <= position {
					var end = getNodeEnd(child)
					if position < end || (position == end && ast.IsTokenNodeAndEqualKind(child, token.EndOfFile)) {
						current = child
						continue outer
					} else if includeItemAtEndPosition != nil && end == position {
						var previousToken = findPrecedingToken(position, sourceFile, child)
						if !isNull(previousToken) && includeItemAtEndPosition(previousToken) {
							return previousToken
						}
					}
				}
			}
		}
		
		// find the child that contains 'offset'
		for _, child := range getNodeChildren(current, nil) {
			var start int
			if allowPositionInLeadingTrivia {
				start = getNodeFullStart(child)
			} else {
				start = getNodeStart(child, sourceFile, includeDocComment)
			}

			if start <= position {
				var end = getNodeEnd(child)
				if position < end || (position == end && ast.IsTokenNodeAndEqualKind(child, token.EndOfFile)) {
					current = child
					continue outer
				} else if includeItemAtEndPosition != nil && end == position {
					var previousToken = findPrecedingToken(position, sourceFile, child)
					if previousToken != nil && includeItemAtEndPosition(previousToken) {
						return previousToken
					}
				}
			}
		}

		return current
	}
	return nil
}

// The token on the left of the position is the token that strictly includes the position
// or sits to the left of the cursor if it is on a boundary. For example
//
//   fo|o               -> will return foo
//   foo <comment> |bar -> will return foo
func findTokenOnLeftOfPosition(file *ast.SourceFile, position int) ast.Node {
	// Ideally, getTokenAtPosition should return a token. However, it is currently
	// broken, so we do a check to make sure the result was indeed a token.
	var tokenAtPosition = getTokenAtPosition(file, position, false)
	if isToken(tokenAtPosition) && position > getNodeStart(tokenAtPosition, file, false) && position < getNodeEnd(tokenAtPosition) {
		return tokenAtPosition
	}

	return findPrecedingToken(position, file, nil)
}

func isInsideComment(sourceFile *ast.SourceFile, token ast.Node, position int) bool {
	var isInsideCommentRange = func (comments []ast.CommentRange) bool {
		for _, comment := range comments {
			// either we are 1. completely inside the comment, or 2. at the end of the comment
			if comment.Pos() < position && position < comment.End() {
				return true
			} else if position == comment.End() {
				var text = sourceFile.Text
				var width = comment.End() - comment.Pos()
				// is single line comment or just /*
				if width <= 2 || text[comment.Pos() + 1] == '/' {
					return true
				} else {
					// is unterminated multi-line comment
					return !(text[comment.End() - 1] == '/' &&
						text[comment.End() - 2] == '*')
				}
			}
			return false
		}
		return false
	}


	// The offset has to be: 1. in the leading trivia (before token.getStart()), and 2. within a comment
	return position <= getNodeStart(token, sourceFile, false) &&
	(isInsideCommentRange(scanner.GetTrailingComments(sourceFile.Text, getNodeFullStart(token))) ||
	isInsideCommentRange(scanner.GetLeadingComments(sourceFile.Text, getNodeFullStart(token))))
}

func isCallExpressionTarget(node ast.Node) bool {
	var target = climbPastSelectorExpressionOrQualifiedNameOrInstanceAccessExpression(node)
	return !isNull(target) && !isNull(target.Parent()) &&
		ast.IsCallExpression(target.Parent()) &&
		target.Parent().(*ast.CallExpression).Expression == target
}

func isDocumentCallExpressionTarget(node ast.Node) bool {
	var target = climbPastSelectorExpressionOrQualifiedNameOrInstanceAccessExpression(node)
	return !isNull(target) && !isNull(target.Parent()) &&
		ast.IsDocumentCallExpression(target.Parent()) &&
		target.Parent().(*ast.DocumentCallExpression).Expression == target
}

func getContainCallExpression(node ast.Node) *ast.CallExpression {
	for !isNull(node) {
		if ast.IsCallExpression(node) {
			return node.(*ast.CallExpression)
		}
		node = node.Parent()
	}
	return nil
}

func getContainDocumentCallExpression(node ast.Node) *ast.DocumentCallExpression {
	for !isNull(node) {
		if ast.IsDocumentCallExpression(node) {
			return node.(*ast.DocumentCallExpression)
		}
		node = node.Parent()
	}
	return nil
}

func isConstructCallExpression(node ast.Node) bool {
	var target = climbPastSelectorExpressionOrQualifiedNameOrInstanceAccessExpression(node)
	return !isNull(target) && !isNull(target.Parent()) &&
		ast.IsConstructorExpression(target.Parent()) &&
		target.Parent().(*ast.ConstructorExpression).TypeName == node
}

func climbPastSelectorExpressionOrQualifiedNameOrInstanceAccessExpression(node ast.Node) ast.Node {
	if isRightSideOfSelectorExpression(node) ||
		isRightSideOfQualifiedName(node) ||
		isRightSideOfInstanceAccessExpression(node) {
		return node.Parent()
	} else {
		return node
	}
}

func isRightSideOfSelectorExpression(node ast.Node) bool {
	return node != nil && node.Parent() != nil &&
		ast.IsSelectorExpression(node.Parent()) &&
		node.Parent().(*ast.SelectorExpression).Name == node
}

func isRightSideOfQualifiedName(node ast.Node) bool {
	return node != nil && node.Parent() != nil &&
		ast.IsQualifiedName(node.Parent()) &&
		node.Parent().(*ast.QualifiedName).Right == node
}

func isRightSideOfInstanceAccessExpression(node ast.Node) bool {
	return node != nil && node.Parent() != nil &&
		ast.IsInstanceAccessExpression(node.Parent()) &&
		node.Parent().(*ast.InstanceAccessExpression).Right == node
}

func isNameOfFunctionDeclaration(node ast.Node) bool {
	return ast.IsIdentifier(node) &&
		ast.IsFunctionLike(node.Parent()) &&
		ast.GetDeclarationName(node.Parent().(ast.Declaration)) == node
}

func isNull(i interface{}) bool {
	ret := i == nil
	if !ret {
		vi := reflect.ValueOf(i)
		kind := reflect.ValueOf(i).Kind()
		if kind == reflect.Slice ||
			kind == reflect.Map ||
			kind == reflect.Chan ||
			kind == reflect.Interface ||
			kind == reflect.Func ||
			kind == reflect.Ptr {
			return vi.IsNil()
		}
	}

	return ret
}

func assert(expr bool){
	if !expr {
		panic("Assert Fail")
	}
}